package com.googlecode.androidutils.database;

import static com.googlecode.androidutils.ArgumentUtils.notEmpty;
import static com.googlecode.androidutils.ArgumentUtils.notNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.googlecode.androidutils.ArgumentUtils;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

/**
 * Builder for a query that can be executed with a {@link SQLiteDatabase}. The
 * builder is designed to provide a fluent interface with invocation chaining to
 * ease the usage of the
 * {@link SQLiteDatabase#query(String, String[], String, String[], String, String, String)}
 * method.
 * 
 * Example usage:
 * 
 * <pre>
 * Cursor cursor = QueryBuilder.from(&quot;table&quot;).select(&quot;_id&quot;, &quot;foo&quot;, &quot;bar&quot;)
 * 		.where(&quot;foo &gt; ?&quot;, 17).orderBy(&quot;bar desc&quot;).execute(database);
 * </pre>
 * 
 * @author Alexander Metzner
 * 
 */
public final class QueryBuilder implements Cloneable {
	private final String table;
	private List<String> columns = new ArrayList<String>();
	private String whereClause;
	private String[] whereArguments;
	private String groupBy;
	private String having;
	private String orderBy;
	private String limit;

	/**
	 * Fluent method for constructing a new builder.
	 * 
	 * @see #QueryBuilder(String)
	 * @param table
	 *            the table to select from
	 * @return the builder
	 */
	public static QueryBuilder from(final String table) {
		return new QueryBuilder(table);
	}

	/**
	 * Private copy constructor.
	 * 
	 * @param table
	 *            the table
	 * @param columns
	 *            the columns
	 * @param whereClause
	 *            the where clause
	 * @param whereArguments
	 *            the where arguments
	 * @param groupBy
	 *            the group by clause
	 * @param having
	 *            the having clause
	 * @param orderBy
	 *            the order by clause
	 * @param limit
	 *            the limit
	 */
	private QueryBuilder(String table, List<String> columns,
			String whereClause, String[] whereArguments, String groupBy,
			String having, String orderBy, String limit) {
		this.table = table;
		this.columns = columns;
		this.whereClause = whereClause;
		this.whereArguments = whereArguments;
		this.groupBy = groupBy;
		this.having = having;
		this.orderBy = orderBy;
		this.limit = limit;
	}

	/**
	 * Constructs a new builder that selects from the given table.
	 * 
	 * @param table
	 *            the table to select from
	 */
	public QueryBuilder(String table) {
		this.table = notEmpty("table", table);
	}

	/**
	 * Select the given columns.
	 * 
	 * @param columns
	 *            the columns to select
	 * @return the builder
	 */
	public QueryBuilder select(final String... columns) {
		this.columns = Arrays.asList(columns);
		return this;
	}

	/**
	 * Appends a where clause.
	 * 
	 * @param whereClause
	 *            the where clause
	 * @param whereArguments
	 *            the positional argument values
	 * @return the builder
	 */
	public QueryBuilder where(final String whereClause,
			final Object... whereArguments) {
		this.whereClause = whereClause;
		this.whereArguments = asStrings(whereArguments);
		return this;
	}

	/**
	 * Groups the results by the given expression.
	 * 
	 * @param groupBy
	 *            the expression
	 * @return the builder
	 */
	public QueryBuilder groupBy(final String groupBy) {
		this.groupBy = groupBy;
		return this;
	}

	/**
	 * Applies a having clause.
	 * 
	 * @param having
	 *            the having clause
	 * @return the builder
	 */
	public QueryBuilder having(final String having) {
		this.having = having;
		return this;
	}

	/**
	 * Appends an ordering.
	 * 
	 * @param orderBy
	 *            the order by clause
	 * @return the builder
	 */
	public QueryBuilder orderBy(final String orderBy) {
		this.orderBy = orderBy;
		return this;
	}

	/**
	 * Sets the limit for the query.
	 * 
	 * @param limit
	 *            the limit
	 * @return the builder
	 */
	public QueryBuilder limit(final String limit) {
		this.limit = limit;
		return this;
	}

	/**
	 * Sets the limit for the query.
	 * 
	 * @param limit
	 *            the limit
	 * @return the builder
	 */
	public QueryBuilder limit(final int limit) {
		return limit(Integer.toString(limit));
	}

	/**
	 * Executes the built query using the given database and returns the
	 * {@link Cursor}. The builder remains valid and may be reused for further
	 * queries.
	 * 
	 * @param database
	 *            the database to execute the query. Must not be
	 *            <code>null</code>.
	 * @return the {@link Cursor}
	 */
	public Cursor query(final SQLiteDatabase database) {
		return notNull("database", database).query(table,
				columns.toArray(new String[columns.size()]), whereClause,
				whereArguments, groupBy, having, orderBy, limit);
	}

	/**
	 * Executes the given query as a delete statement.
	 * 
	 * @param database
	 *            the database to execute the query. Must not be
	 *            <code>null</code>.
	 * @return the number of affected rows
	 */
	public int delete(final SQLiteDatabase database) {
		return notNull("database", database).delete(table, whereClause,
				whereArguments);
	}

	/**
	 * Executes the given query as an update statement.
	 * 
	 * @param database
	 *            the database to execute the query. Must not be
	 *            <code>null</code>.
	 * @param values
	 *            the value to set
	 * @return the number of affected rows
	 */
	public int update(final SQLiteDatabase database, final ContentValues values) {
		notNull("database", database);
		notNull("values", values);

		return database.update(table, values, whereClause, whereArguments);
	}

	/**
	 * @return a deep clone of this builder.
	 */
	@Override
	protected QueryBuilder clone() {
		return new QueryBuilder(table, columns, whereClause, whereArguments,
				groupBy, having, orderBy, limit);
	}

	/**
	 * Converts the given objects to an array of string by calling
	 * {@link #toString()} on each object given.
	 * 
	 * @param objects
	 *            the objects to convert
	 * @return the array of strings
	 */
	private static String[] asStrings(final Object... objects) {
		final List<String> result = new ArrayList<String>();
		for (final Object object : objects) {
			result.add(object.toString());
		}
		return result.toArray(new String[result.size()]);
	}
}
